package com.snda.sc.auth.simple;

import java.io.File;
import java.nio.file.Files;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.snda.sc.auth.Auth;
import com.snda.sc.auth.AuthParam;
import com.snda.sc.auth.AuthResult;
import com.snda.sc.auth.RedirectResult;
import com.snda.sc.auth.data.GameHost;
import com.snda.sc.auth.data.GamePlayer;
import com.snda.sc.auth.data.UserInfo;
import com.snda.sc.hps.HPSOption;
import com.snda.sc.hps.IHpsBinder;
import com.snda.sc.store.IRedisStore;
import com.snda.sc.utils.Charsets;
import com.snda.sc.utils.CommonUtils;
import com.snda.sc.utils.Constants.ResultCode;
import com.snda.sc.utils.Keys;
import com.snda.sc.utils.Logs;
import com.snda.sc.utils.Signs;

/**
 * Auth的简单实现，针对baidu、vgame、2133等标准接入流程进行了封装。<br>
 * 标准流程：<br>
 * <ol>
 * <li>用户进入游戏</li>
 * <li>第三方平台跳转到SC的地址，并传递api_key、user_id、server_id、cm_flag、timestamp、sign等信息</li>
 * <li>SC进行sign值效验、HPS绑定、结果写入Redis等操作</li>
 * <li>跳转到游戏页面地址并传递AuthResult参数</li>
 * </ol>
 * 
 * @author <a href="mailto:seenoevil.cn@gmail.com">李曦</a><br/>
 *         Created at: 2012-10-3 上午9:32:21
 */
public abstract class SimpleAuth implements Auth {

	public static final String AUTH_CONFIG_FILENAME = "auth.json";

	@Resource(name = "redisStore")
	protected IRedisStore redisStore;
	@Resource(name = "hpsBinder")
	protected IHpsBinder hpsBinder;

	@Value("${sc.plt}")
	protected String plt;

	protected SimpleAppInfoReader appInfoReader = new SimpleAppInfoReader();

	// 配置文件中的属性值
	protected ConcurrentMap<String, AppInfo> appInfos = new ConcurrentHashMap<>();

	@Override
	public void configure(File confFile) throws Exception {
		JSONArray confJsons = JSON.parseArray(new String(Files.readAllBytes(confFile.toPath()), Charsets.UTF_8));
		for (int i = 0; i < confJsons.size(); i++) {
			JSONObject confJson = confJsons.getJSONObject(i);
			AppInfo appInfo = appInfoReader.readConfItem(confJson);
			appInfos.put(appInfo.getKey(), appInfo);
		}
	}

	@Override
	public Set<String> getConfigFileNames() {
		return CommonUtils.initSet(AUTH_CONFIG_FILENAME);
	}

	@Override
	public boolean watchModify() {
		return true;
	}

	@Override
	public Scope getScope() {
		return Scope.APP;
	}

	@Override
	public AuthResult doAuth(AuthParam param) {
		SimpleParamNameBinder binder = new SimpleParamNameBinder(param, true);
		bindParamName(binder);

		AppInfo appInfo = appInfos.get(binder.getKey());
		// Prevent NullPointException
		if (appInfo == null) appInfo = new AppInfo();

		String appUrl = getAppUrl(appInfo);
		GameHost host = appInfo.getGameHosts().get(binder.getServerId());

		AuthResult result = new RedirectResult(host, appUrl);
		result.setResult(ResultCode.AUTH_OK);
		result.setPlt(plt);

		if (host == null) {
			result.setResult(ResultCode.AUTH_ERR_CONF);
			result.setCause(String.format("No gameHost config item found by: key{%s}, serverId{%s}.", binder.getKey(),
					binder.getServerId()));
			return result;
		}

		// check sign
		String expectSign = binder.getSign();
		String actualSign = Signs.createSignWithNoSymbolSecretFront(binder.getSignDict(), appInfo.getSecret());
		if (!expectSign.equals(actualSign)) {
			result.setResult(ResultCode.AUTH_ERR_SIGN);
			result.setCause(String.format("Sign Failure, expect sign: %s, actual sign: %s.", expectSign, actualSign));
			return result;
		}

		// bind hps if need
		if (appInfo.isBindHps()) {
			HPSOption option = new HPSOption(plt, appInfo.getAppId(), isAdult(binder.getCmFlag()), binder.getUserId(),
					param.getClientIp(), host.getAppArea());
			hpsBinder.bind(option, result);
		} else {
			result.setUid("t.uid." + CommonUtils.uuid());
			result.setPt("t.pt." + CommonUtils.uuid());
		}

		if (result.getResult() != ResultCode.AUTH_OK) return result;

		if (!saveAuthResult(result, binder.getUserId(), binder.getCmFlag())) return result;

		result.setAppName(appInfo.getAppName());
		result.setCheckUrl(appInfo.getCheckUrl());
		result.setAppUrl(appInfo.getAppUrl());
		result.setSnsUid(binder.getUserId());

		postAuth(result, binder, appInfo);

		return result;
	}

	@Override
	public AuthResult createFailureResult(AuthParam param) {
		SimpleParamNameBinder binder = new SimpleParamNameBinder(param, false);
		bindParamName(binder);

		String key = binder.getKey();
		AppInfo appInfo = (key == null) ? null : appInfos.get(key);
		if (appInfo == null) appInfo = new AppInfo();
		String appUrl = getAppUrl(appInfo);

		return new RedirectResult(null, appUrl);
	}

	private String getAppUrl(AppInfo appInfo) {
		String appUrl = appInfo.getAppUrl();
		return StringUtils.hasLength(appUrl) ? appUrl : getDefaultAppUrl();
	}

	protected void postAuth(AuthResult result, SimpleParamNameBinder binder, AppInfo appInfo) {
	}

	protected void bindParamName(SimpleParamNameBinder binder) {
		binder.bindCmFlagName("cm_flag");
		binder.bindKeyName("api_key");
		binder.bindServerIdName("server_id");
		binder.bindSignName("sign");
		binder.bindTimestampName("timestamp");
		binder.bindUserIdName("user_id");
	}

	protected boolean saveAuthResult(AuthResult result, String snsUid, String cmFlag) {
		String ticket = Keys.newTicket(snsUid);

		String uid = result.getUid();
		String pt = result.getPt();
		String code = Keys.newCode(uid, ticket);

		UserInfo userInfo = new UserInfo(uid, snsUid, null);
		GamePlayer gamePlayer = new GamePlayer(pt, null, Integer.parseInt(isAdult(cmFlag)), null);
		try {
			redisStore.setCodeExpire(Keys.uidKey(plt, code), uid);
			redisStore.setCodeExpire(Keys.ticketKey(plt, uid), ticket);

			redisStore.set(Keys.userInfoKey(plt, uid), JSON.toJSONString(userInfo));
			redisStore.set(Keys.friendInfoKey(plt, uid), JSON.toJSONString(gamePlayer));
		} catch (Exception e) {
			result.setResult(ResultCode.AUTH_ERR_REDIS);
			result.setCause("Write AuthResult in redis error.");
			Logs.logException("Write AuthResult in redis error.", e);
			return false;
		}

		result.setCode(code);
		return true;
	}

	protected String isAdult(String cmFlag) {
		return "n".equals(cmFlag) ? HPSOption.ISADULT_Y : HPSOption.ISADULT_N;
	}

	protected abstract String getDefaultAppUrl();
}
